package com.workingpub.commons.jdbc.internal;

import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.sql.DataSource;

import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.workingpub.commons.datasource.DataSourceConstants;
import com.workingpub.commons.datasource.DatabaseType;
import com.workingpub.commons.jdbc.JdbcManager;
import com.workingpub.commons.jdbc.JdbcSourceFactory;
import com.workingpub.commons.jdbc.SqlClientManager;
import com.workingpub.commons.jdbc.SqlDefineContext;
import com.workingpub.commons.jdbc.builder.SqlMappingBuilder;
import com.workingpub.commons.jdbc.config.SqlDefineResolver;
import com.workingpub.commons.jdbc.exception.JdbcAccessException;
import com.workingpub.commons.orm.EntityManager;
import com.workingpub.commons.orm.internal.JdbcEntityManager;

public class DefaultJdbcSourceFactory implements JdbcSourceFactory {
	private static final Logger logger = LoggerFactory.getLogger(DefaultJdbcSourceFactory.class);
	private static final Lock lock = new ReentrantLock();

	private SqlDefineContext sqlDefineContext;
	private SqlDefineResolver sqlDefineResolver;
	private SqlMappingBuilder sqlMappingBuilder;
	private SqlMappingListener sqlMappingListener;

	private BundleContext bundleContext;

	public void setSqlDefineContext(SqlDefineContext sqlDefineContext) {
		this.sqlDefineContext = sqlDefineContext;
	}

	public void setSqlDefineResolver(SqlDefineResolver sqlDefineResolver) {
		this.sqlDefineResolver = sqlDefineResolver;
	}

	public void setSqlMappingBuilder(SqlMappingBuilder sqlMappingBuilder) {
		this.sqlMappingBuilder = sqlMappingBuilder;
	}

	public void setSqlMappingListener(SqlMappingListener sqlMappingListener) {
		this.sqlMappingListener = sqlMappingListener;
	}

	private BundleContext getBundleContext() {
		if (null == bundleContext) {
			try {
				lock.lock();
				if (null == bundleContext) {
					bundleContext = FrameworkUtil.getBundle(DefaultJdbcSourceFactory.class).getBundleContext();
				}
			} finally {
				lock.unlock();
			}
		}
		return bundleContext;
	}

	private JdbcManager generateJdbcManager(String domainCode) {
		String filter = "(" + DataSourceConstants.Property_XA_JNDI + "=" + DataSourceConstants.PREFIX_XA_JNDI
				+ (StringUtils.hasText(domainCode) ? domainCode : DataSourceConstants.Platform_Domain_Code) + ")";
		ServiceReference<DataSource> reference = findServiceReference(DataSource.class, filter);
		Assert.notNull(reference, "The datasource is not exists, domain code : " + domainCode);
		DatabaseType dbtype = DatabaseType
				.valueOf((String) reference.getProperty(DataSourceConstants.Property_DatabaseType));
		AbstractJdbcManager jdbcManager = null;
		if (DatabaseType.MySQL.equals(dbtype)) {
			jdbcManager = new MySqlJdbcManager();
		} else if (DatabaseType.Oracle.equals(dbtype)) {
			jdbcManager = new OracleJdbcManager();
		} else {
			throw new JdbcAccessException("jdbc.ds.unknown",
					("The data source type is unknown, domain code : " + domainCode));
		}
		DataSource dataSource = getBundleContext().getService(reference);
		NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
		jdbcManager.setMainJdbcTemplate(jdbcTemplate);

		Dictionary<String, String> properties = getFilter(domainCode);
		getBundleContext().registerService(JdbcManager.class, jdbcManager, properties);

		return jdbcManager;
	}

	private EntityManager generateEntityManager(String domainCode) {
		JdbcManager jdbcManager = createJdbcManager(domainCode);
		JdbcEntityManager entityManager = new JdbcEntityManager();
		entityManager.setJdbcManager(jdbcManager);

		Dictionary<String, String> properties = getFilter(domainCode);
		getBundleContext().registerService(EntityManager.class, entityManager, properties);

		return entityManager;
	}

	private SqlClientManager generateSqlMappingManager(String domainCode) {
		JdbcManager jdbcManager = createJdbcManager(domainCode);
		DefaultSqlClientManager sqlClientManager = new DefaultSqlClientManager();
		sqlClientManager.setJdbcManager(jdbcManager);
		sqlClientManager.setSqlDefineContext(sqlDefineContext);
		sqlClientManager.setSqlDefineResolver(sqlDefineResolver);
		sqlClientManager.setSqlMappingBuilder(sqlMappingBuilder);
		sqlClientManager.setSqlMappingListener(sqlMappingListener);

		Dictionary<String, String> properties = getFilter(domainCode);
		getBundleContext().registerService(SqlClientManager.class, sqlClientManager, properties);

		return sqlClientManager;
	}

	protected Dictionary<String, String> getFilter(String domainCode) {
		Dictionary<String, String> properties = new Hashtable<>();
		properties.put(Filter_JDBC_Domain_Key,
				(StringUtils.hasText(domainCode) ? domainCode : DataSourceConstants.Platform_Domain_Code));
		return properties;
	}

	protected <T> ServiceReference<T> findServiceReference(Class<T> serviceType, String filter) {
		logger.info("Dynamic create jdbc source : " + serviceType + "\t" + filter);
		Collection<ServiceReference<T>> references = null;
		try {
			references = getBundleContext().getServiceReferences(serviceType, filter);
		} catch (InvalidSyntaxException e) {
			logger.error("Find service error : " + e.getMessage());
			throw new JdbcAccessException("jdbc.source.find", "Find service error : " + e.getMessage(), e);
		}
		if (CollectionUtils.isEmpty(references)) {
			return null;
		} else {
			if (references.size() > 1) {
				throw new JdbcAccessException("jdbc.source.find",
						"Find more than one servcie by filter : " + filter + "\t" + references.size());
			}
			ServiceReference<T> reference = references.iterator().next();
			return reference;
		}
	}

	@Override
	public JdbcManager createJdbcManager(String domainCode) {
		String filter = "(" + Filter_JDBC_Domain_Key + "="
				+ (StringUtils.hasText(domainCode) ? domainCode : DataSourceConstants.Platform_Domain_Code) + ")";
		ServiceReference<JdbcManager> reference = findServiceReference(JdbcManager.class, filter);
		if (null != reference) {
			return getBundleContext().getService(reference);
		} else {
			return generateJdbcManager(domainCode);
		}
	}

	@Override
	public EntityManager createEntityManager(String domainCode) {
		String filter = "(" + Filter_JDBC_Domain_Key + "="
				+ (StringUtils.hasText(domainCode) ? domainCode : DataSourceConstants.Platform_Domain_Code) + ")";
		ServiceReference<EntityManager> reference = findServiceReference(EntityManager.class, filter);
		if (null != reference) {
			return getBundleContext().getService(reference);
		} else {
			return generateEntityManager(domainCode);
		}
	}

	@Override
	public SqlClientManager createSqlMappingManager(String domainCode) {
		String filter = "(" + Filter_JDBC_Domain_Key + "="
				+ (StringUtils.hasText(domainCode) ? domainCode : DataSourceConstants.Platform_Domain_Code) + ")";
		ServiceReference<SqlClientManager> reference = findServiceReference(SqlClientManager.class, filter);
		if (null != reference) {
			return getBundleContext().getService(reference);
		} else {
			return generateSqlMappingManager(domainCode);
		}
	}

}
